package com.streambolics.servlet;

import java.io.IOException;
import java.util.Calendar;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

/**
 * The base class of all well-behaved servlets.
 * 
 * A well-behaved servlet is basically stateful. As such, it automatically
 * establishes a critical section while generating its output. While this may
 * severely impact performance, it also ensures that updates to model objects
 * are a little less subject to race conditions.
 * 
 * The statefulness is required by separation of concerns. This allows the base
 * class to provide features and services to lower level classes, without
 * requiring the lower classes to know about the implementation of those
 * services. This in turn requires the base class to store state information for
 * those services, without requiring the derived classes to provide storage for
 * that state.
 * 
 * Statefulness is achieved by calling three functions in turn:
 * setupEnvironment(), buildResponse() and teardownEnvironment().
 * 
 * The bulk of the processing must be performed in buildResponse(). Most of the
 * utility functions assume that they are called as part of the buildResponse()
 * processing. setupEnvironment() is used to store state information at each
 * inheritance level, and teardownEnvironment() cleans the state.
 * 
 * It is important to remember that servlets are normally kept alive for longer
 * than the handling of a single request, so the setup and teardown must ensure
 * that all remnants of a previous request are properly disposed of before
 * proceeding.
 * 
 * @author Sauron
 * 
 */

@SuppressWarnings("serial")
public abstract class StandardServlet extends HttpServlet
{
    private HttpServletRequest  _request;

    private HttpServletResponse _response;

    private static String nonEmpty (String s)
    {
        if (s == null)
        {
            return null;
        }
        s = s.trim ();
        if (s.isEmpty ())
        {
            return null;
        }
        return s;
    }

    /**
     * Builds the response.
     * 
     * The response is built after the environment has been built by
     * setupEnvironment(). After the response has been built, the environment is
     * destroyed using teardownEnvironment().
     * 
     * It is guaranteed that a single buildResponse is running at any given time
     * for a given servlet, but multiple servlets may run concurrently. This
     * means that all instance objects are thread safe.
     * 
     * @param aRequest
     *            The HttpRequest to handle.
     * @param aResponse
     *            The HttpResponse to give.
     * @throws Exception
     *             As per Streambolics Core conventions
     */

    protected abstract void buildResponse (HttpServletRequest aRequest, HttpServletResponse aResponse) throws Exception;

    /**
     * Removes a session variable.
     * 
     * @param aName
     *            The name of the session variable to remove
     */

    protected void clearSession (String aName)
    {
        getSession ().removeAttribute (aName);
    }

    @Override
    protected final void doGet (HttpServletRequest aRequest, HttpServletResponse aResponse) throws ServletException, IOException
    {
        execute (aRequest, aResponse);
    }

    protected final synchronized void execute (HttpServletRequest aRequest, HttpServletResponse aResponse) throws ServletException, IOException
    {
        try
        {
            setupEnvironment (aRequest, aResponse);
            try
            {
                buildResponse (aRequest, aResponse);
            }
            finally
            {
                teardownEnvironment ();
            }
        }
        catch (MissingInformationException e)
        {
            try
            {
                if (!e.formatError (aResponse))
                {
                    throw e;
                }
            }
            catch (Exception e1)
            {
                throw new ServletException ("Whoa ! Unexpected exception " + e1.getMessage () + " while trying to handle " + e.getMessage ());
            }
        }
        catch (ServletException e)
        {
            throw e;
        }
        catch (IOException e)
        {
            throw e;
        }
        catch (Exception e)
        {
            // TODO : Log the exception here.
            // TODO : It might be better to generate a custom page here
            throw new ServletException ("Generic exception occurred", e);
        }
    }

    /**
     * The value of a header field.
     * 
     * @param aName
     *            The name of the header field to retrieve.
     * @return The string value of the field, or null if the field is not found
     *         or empty.
     * 
     */

    protected String getHeader (String aName)
    {
        return nonEmpty (_request.getHeader (aName));
    }

    /**
     * The value of a parameter field.
     * 
     * A parameter field is embedded in the URL after the question mark.
     * 
     * @param aName
     *            The name of the parameter field to retrieve.
     * @return The string value of the field, or null if the field is not found
     *         or empty.
     */

    protected String getParameter (String aName)
    {
        return nonEmpty (_request.getParameter (aName));
    }

    /**
     * The Http request.
     * 
     * @return
     */

    protected HttpServletRequest getRequest ()
    {
        return _request;
    }

    /**
     * The HTTP response.
     * 
     * @return
     */

    protected HttpServletResponse getResponse ()
    {
        return _response;
    }

    /**
     * An URL to return to this page.
     * 
     * @return The string representation of the URL needed to come back to this
     *         page.
     */

    protected String getReturnUrl ()
    {
        String q = _request.getQueryString ();
        if (q == null)
        {
            return _request.getRequestURI ();
        }
        else
        {

            return _request.getRequestURI () + "?" + q;
        }
    }

    /**
     * The current session.
     * 
     * @return
     */

    protected HttpSession getSession ()
    {
        return _request.getSession (true);
    }

    /**
     * The value of a session variable.
     * 
     * @param aName
     *            The name of the variable.
     * 
     * @return The object value, of null if the variable is not found.
     * 
     */

    protected Object getSession (String aName)
    {
        return getSession ().getAttribute (aName);
    }

    private void parseRequiredCalendarField (String aName, Calendar aCalendar, int aField, int aOffset) throws Exception
    {
        aCalendar.set (aField, requireIntArg (aName) + aOffset);
    }

    /**
     * Parse URL parameters representing a date, and set the corresponding
     * fields in a Calendar object.
     * 
     * @param aDateName
     *            The name of the date parameters. Actual parameters use this
     *            name suffixed with _YR, _MO and _DY.
     * @param aCalendar
     *            The Calendar object to update.
     * @throws Exception
     *             As per Streambolics Core conventions. An exception is thrown
     *             if the parameters are missing from the query string, cannot
     *             be parsed as integers, or are not within the appropriate
     *             range.
     */

    protected void parseRequiredDate (String aDateName, Calendar aCalendar) throws Exception
    {
        parseRequiredCalendarField (aDateName + "_YR", aCalendar, Calendar.YEAR, 0);
        parseRequiredCalendarField (aDateName + "_MO", aCalendar, Calendar.MONTH, -1);
        parseRequiredCalendarField (aDateName + "_DY", aCalendar, Calendar.DAY_OF_MONTH, 0);
    }

    /**
     * Parse URL parameters representing a time, and set the corresponding
     * fields in a Calendar object.
     * 
     * @param aDateName
     *            The name of the time parameters. Actual parameters use this
     *            name suffixed with _HR and _MI.
     * @param aCalendar
     *            The Calendar object to update.
     * @throws Exception
     *             As per Streambolics Core conventions. An exception is thrown
     *             if the parameters are missing from the query string, cannot
     *             be parsed as integers, or are not within the appropriate
     *             range.
     */

    protected void parseRequiredTime (String aTimeName, Calendar aCalendar) throws Exception
    {
        parseRequiredCalendarField (aTimeName + "_HR", aCalendar, Calendar.HOUR_OF_DAY, 0);
        parseRequiredCalendarField (aTimeName + "_MI", aCalendar, Calendar.MINUTE, 0);
        aCalendar.set (Calendar.SECOND, 0);
        aCalendar.set (Calendar.MILLISECOND, 0);
    }

    /**
     * Retrieves an integer parameter from the query string.
     * 
     * @param name
     *            The name of the parameter.
     * @return The value of the parameter as an integer.
     * @throws Exception
     *             As per Streambolics Core conventions. An exception is thrown
     *             if the parameter is not present in the query string, or if it
     *             cannot be parsed to an integer value.
     */

    protected int requireIntArg (String name) throws Exception
    {
        try
        {
            String val = _request.getParameter (name);
            if (val == null)
            {
                throw new Exception ("Argument not present on the command line");
            }
            try
            {
                return Integer.parseInt (val);
            }
            catch (Exception e)
            {
                throw new Exception ("Unable to parse " + val + " as a long value", e);
            }
        }
        catch (Exception e)
        {
            throw new Exception ("Unable to obtain argument " + name, e);
        }
    }

    protected long requireLongArg (String name) throws Exception
    {
        try
        {
            String val = _request.getParameter (name);
            if (val == null)
            {
                throw new Exception ("Argument not present on the command line");
            }
            try
            {
                return Long.parseLong (val);
            }
            catch (Exception e)
            {
                throw new Exception ("Unable to parse " + val + " as a long value", e);
            }
        }
        catch (Exception e)
        {
            throw new Exception ("Unable to obtain argument " + name, e);
        }
    }

    /**
     * The value of the ID parameter.
     * 
     * @return The value of the ID parameter from the URL.
     * @throws Exception
     *             As per Streambolics Core conventions. Thrown if the parameter
     *             is not found, or cannot be parsed into a long integer.
     */
    protected long requireReferenceId () throws Exception
    {
        return requireLongArg ("ID");
    }

    /**
     * Set the value of a session variable.
     * 
     * @param aName
     *            The name of the variable.
     * @param aValue
     *            The value to assign.
     */

    protected void setSession (String aName, Object aValue)
    {
        getSession ().setAttribute (aName, aValue);
    }

    /**
     * Setup the environment so that request processing can be done.
     * 
     * @param aRequest
     *            The HTTP request.
     * @param aResponse
     *            The HTTP response.
     * @throws Exception
     *             As per Streambolics Core conventions.
     */

    protected void setupEnvironment (HttpServletRequest aRequest, HttpServletResponse aResponse) throws Exception
    {
        _request = aRequest;
        _response = aResponse;
    }

    /**
     * Clean the environment after request processing is done.
     * 
     * This function must be redefined at each inheritance level to cleanup
     * cached data. This function will be called even if buildResponse() failed,
     * but will not be called if setupEnvironment() failed.
     * 
     * @throws Exception
     */

    protected void teardownEnvironment () throws Exception
    {
    }

}
